/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.geode.internal.util;

import org.apache.geode.distributed.internal.DistributionConfig;
import org.apache.geode.i18n.StringId;
import org.apache.geode.internal.cache.EventID;

/**
 * Breadcrumbs establishes traces in thread names that are useful in figuring
 * out what is going on in a distributed system given only stack traces.
 * 
 * @since GemFire 20 May 2014
 *
 */
public class Breadcrumbs {

  private static ThreadLocal<EventID> EventIDs = new ThreadLocal<EventID>();

  public static boolean ENABLED = Boolean.getBoolean(DistributionConfig.GEMFIRE_PREFIX + "enable-breadcrumbs");
  
  
  /** delimiter for crumb numbers */
  final static String CrumbDelimiter = "/";

  /** string that starts a crumb */
  final static String CommonBreadcrumbStart = "\n\t" + CrumbDelimiter;

  /** all known types of breadcrumbs */
  private enum CrumbType {
    RECEIVE_SIDE,
    EVENTID,
    SEND_SIDE,
    PROBLEM
  }
  
  /** crumb with the highest ordinal, for initialization of delimiter strings */
  private static CrumbType Crumbiest = CrumbType.PROBLEM;
  
  private static String[] crumbLabels = new String[] {
    "rcv",
    "evt",
    "snd",
    "oops"
  };
  
    /** strings that start a particular breadcrumb */
  private static String[] crumbStarts = new String[Crumbiest.ordinal()+1];
  
  /** strings the terminate a particular breadcrumb */
  private static String[] crumbEnds = new String[Crumbiest.ordinal()+1];
  
  
  static {
    // initialize breadcrumb delimiter strings
    for (int i=0; i <= Crumbiest.ordinal(); i++) {
      crumbStarts[i] = CommonBreadcrumbStart + crumbLabels[i] + CrumbDelimiter + " ";
      crumbEnds[i] = " " + CrumbDelimiter + crumbLabels[i] + CrumbDelimiter;
    }
  }

  
  ////////////////////////  P U B L I C   A P I
  
  
  /** set the "sender" breadcrumb for a message while it's being processed */
  public static void setReceiveSide(Object crumb) {
    if (ENABLED) {
      setBreadcrumb(Thread.currentThread(), CrumbType.RECEIVE_SIDE, crumb);
    }
  }
  
  /** set the "recipients" breadcrumb for a message while waiting for it */
  public static void setSendSide(Object crumb) {
    if (ENABLED) {
      setBreadcrumb(Thread.currentThread(), CrumbType.SEND_SIDE, crumb);
    }
  }

  /** sets the given breadcrumb into the name of the current thread */
  public static void setEventId(Object crumb) {
    if (ENABLED) {
      if (crumb instanceof EventID) {
        EventID other = EventIDs.get();
        if (other != null && other != crumb) {
          ((EventID)crumb).incBreadcrumbCounter();
          setBreadcrumb(Thread.currentThread(), CrumbType.EVENTID, crumb);
        }
      }
      else {
        setBreadcrumb(Thread.currentThread(), CrumbType.EVENTID, crumb);
      }
    }
  }
  
  /**
   * a problem crumb can be set using I18n message strings and arguments.
   * Breadcrumb will localize the string with the given args
   */
  public static void setProblem(StringId msg, Object[] args) {
    if (ENABLED) {
      setBreadcrumb(Thread.currentThread(), CrumbType.PROBLEM, msg.toLocalizedString(args));
    }
  }

  /** sets the given breadcrumb into the name of the current thread */
  public static void setProblem(Object crumb) {
    if (ENABLED) {
      setBreadcrumb(Thread.currentThread(), CrumbType.PROBLEM, crumb);
    }
  }
  
  /** clears the breadcrumbs from the name of the current thread */
  public static void clearBreadcrumb() {
    if (ENABLED) {
      clearBreadcrumb(Thread.currentThread());
      EventIDs.set(null);
    }
  }

  
  
  /////////////////////// E N D   O F   P U B L I C   A P I

  
  /** sets the given breadcrumb into the name of the given thread */ 
  private static void setBreadcrumb(Thread t, CrumbType type, Object crumb) {
    setCrumbInThread(t, type, crumb);
  }
  
  /** clears the breadcrumb from the name of the given thread */
  private static void clearBreadcrumb(Thread t) {
    String name = t.getName();
    int i = name.indexOf(CommonBreadcrumbStart);
    if (i >= 0) {
      t.setName(name.substring(0, i));
    }
  }
  

  /**
   * This method does all of the work of setting/clearing individual
   * breadcrumbs in thread names
   * 
   * @param t           the thread to modify
   * @param type        the type of breadcrumb
   * @param crumb       the crumb to insert, or null to clear
   */
  private static void setCrumbInThread(Thread t, CrumbType type, Object crumb) {

    int typeIndex = type.ordinal();
    String name = t.getName();
    String crumbString;
    
    if (crumb == null) {
      crumbString = ""; // remove existing crumb, if any
    }
    else {
      crumbString = crumbStarts[typeIndex] + crumb + crumbEnds[typeIndex];
    }
    
    int startIndex = name.indexOf(crumbStarts[typeIndex]);
    int endIndex = -1;
    
    if (startIndex < 0) {
      // there may be a higher numbered breadcrumb already in the thread name
      for (int i = typeIndex+1;  (startIndex < 0)  &&  (i <= Crumbiest.ordinal());  i++) {
        startIndex = name.indexOf(crumbStarts[i]);
        if (startIndex >= 0) {
          endIndex = name.indexOf(crumbEnds[i], startIndex+1);
        }
      }
    }
    
    if (startIndex < 0) {
      // no equal or higher numbered breadcrumbs - just tack this
      // one onto the end of the thread's name
      t.setName(name + crumbString);
    }
    else if (endIndex > 0) {
      // insert before the higher-numbered breadcrumb
      t.setName(name.substring(0, startIndex)
          + crumbString
          + name.substring(startIndex, name.length()));
    }
    else {
      // replace the existing breadcrumb
      endIndex = name.indexOf(crumbEnds[typeIndex], startIndex+1);
      
      // this shouldn't happen
      assert endIndex > 0: "odd thread name: " + name;
      
      t.setName(name.substring(0, startIndex)
          + crumbString
          + name.substring(endIndex+crumbEnds[typeIndex].length(), name.length()));
    }
  }
  

  private static void clearCrumbInThread(Thread t) {
    String name = t.getName();
    int i = name.indexOf(CommonBreadcrumbStart);
    if (i >= 0) {
      t.setName(name.substring(0, i));
    }
  }
  
  public static void main(String args[]) {
    String threadName = null;
    
    ENABLED = true;

    setReceiveSide("processorId=17332");
    threadName = Thread.currentThread().getName();
    System.out.println("thread name with sender=" + threadName);

    setEventId("eventId");
    threadName = Thread.currentThread().getName();
    System.out.println("thread name with eventId=" + threadName);

    setEventId("eventId2");
    threadName = Thread.currentThread().getName();
    System.out.println("replaced eventId: " + threadName);
    
    setSendSide("recipients");
    threadName = Thread.currentThread().getName();
    System.out.println("with recipients: " + threadName);
    
    setEventId("eventId3");
    threadName = Thread.currentThread().getName();
    System.out.println("replaced eventId3: " + threadName);
    
    setSendSide("recipients2");
    threadName = Thread.currentThread().getName();
    System.out.println("with recipients2: " + threadName);
    
    setReceiveSide("processorId=22222");
    threadName = Thread.currentThread().getName();
    System.out.println("thread name with processor 22222=" + threadName);
    
    setSendSide(null);
    threadName = Thread.currentThread().getName();
    System.out.println("with recipients removed: " + threadName);

    clearBreadcrumb();
    threadName = Thread.currentThread().getName();
    System.out.println("thread name cleared=" + threadName);
  }
  
}
